Различные методы семантического сходства предложений в НЛП
Семантическое сходство — это сходство между двумя словами или двумя предложениями/фразами/текстом. Оно измеряет, насколько близки или различны два фрагмента слова или текста с точки зрения их значения и контекста.
В этой статье мы сосредоточимся на том, как выводится семантическое сходство между двумя предложениями. Мы рассмотрим следующие наиболее используемые модели.
- Dov2Vec — расширение word2vec
- SBERT — модель на основе трансформатора, в которой кодирующая часть фиксирует значение слов в предложении.
- InferSent — использует двунаправленную LSTM-память для кодирования предложений и выведения семантики.
- USE (универсальный кодировщик предложений) — это обученная Google модель, которая генерирует вложения фиксированного размера для предложений, которые можно использовать для любой задачи обработки естественного языка.
Что такое семантическое сходство?
Семантическое сходство относится к степени сходства между словами. Основное внимание уделяется структуре и лексическому сходству слов и фраз. Семантическое сходство углубляется в понимание и значение содержания. Цель сходства — измерить, насколько тесно связаны или аналогичны концепции, идеи или информация, переданные в двух текстах.
В НЛП семантическое сходство используется в различных задачах, таких как
- Вопросы и ответы — улучшает систему контроля качества, устанавливая семантическое сходство между запросами пользователей и содержимым документа.
- Рекомендательные системы - Семантическое сходство между пользовательским контентом и доступным контентом
- Резюмирование — помогает в резюмировании ответов на вопросы по схожему содержанию и сопоставлении текстов.
- Кластеризация корпусов — помогает группировать документы со схожим содержанием.
Существуют определенные подходы к измерению семантического сходства в обработке естественного языка (NLP), которые включают в себя встраивание слов, встраивание предложений и модели преобразователя.
Встраивание слов
Чтобы понять семантические связи между предложениями, нужно знать о вложениях слов . Вложения слов используются для векторного представления слов. Простейшая форма вложения слов — это one-hot вектор. Однако они разрежены, очень многомерны и не отражают смысл. Более продвинутая форма состоит из Word2Vec ( skip-gram , cbow ), GloVe и Fasttext, которые фиксируют семантическую информацию в низкомерном пространстве. Пожалуйста, посмотрите на встроенную ссылку, чтобы глубже понять это.
Word2Vec
Word2Vec представляет слова как многомерные векторы, так что мы получаем семантически похожие слова, близкие друг к другу в векторном пространстве. Существуют две основные архитектуры для Word2Vec:
- Непрерывный мешок слов: цель — предсказать целевое слово на основе контекста окружающих слов.
- Skip-gram: Модель предназначена для прогнозирования окружающих слов в контексте.
Doc2Vec
Подобно word2vec, Doc2Vec имеет два типа моделей, основанных на skip gram и CBOW. Мы рассмотрим модель на основе skip gram, поскольку она работает лучше, чем модель на основе cbow. Эта модель на основе skip-gram называется ad PV-DM (Distributed Memory Model of Paragraph Vectors) .

PV-DM является расширением Word2Vec в том смысле, что он состоит из одного вектора абзаца в дополнение к векторам слов.
- Вектор абзаца и вектор слова : Предположим, что в корпусе N абзацев и M слов в словаре.
- Теперь на основе строк word2vec у нас будет матрица M*Q (Q — размерность вложения слов), которая будет нашей матрицей вложения для вложения слов. Кроме того, у нас будет матрица N*P для наших абзацев (p — размерность вложения абзацев).
- Вектор абзаца является общим для всех контекстов, созданных из одного и того же абзаца, но не для разных абзацев.
- Матрица векторов слов W является общей для всех абзацев.
- Усреднение или конкатенация : чтобы предсказать следующее слово в контексте, вектор абзаца и векторы слов объединяются с помощью усреднения или конкатенации.
- Модель распределенной памяти (PV-DM): токен абзаца действует как память, которая сохраняет информацию о том, чего не хватает в текущем контексте или теме абзаца.
- Обучение с помощью стохастического градиентного спуска : стохастический градиентный спуск используется для обучения векторов абзацев и векторов слов. Градиент получается с помощью обратного распространения. На каждом шаге стохастического градиентного спуска из случайного абзаца выбирается контекст фиксированной длины, а градиент ошибки вычисляется для обновления параметров модели.
- Вывод во время прогнозирования : после обучения модели векторы абзацев отбрасываются, и сохраняются только векторы слов и веса softmax.
- Для нахождения вектора абзаца нового текста Во время прогнозирования выполняется шаг вывода. Это также достигается с помощью градиентного спуска . На этом шаге векторы слов (W) и веса softmax фиксируются, в то время как вектор абзаца изучается с помощью обратного распространения.
- Для двух предложений, для которых нам нужно найти сходство, получаются два вектора абзацев и на основе сходства между двумя векторами получают сходство между предложениями.
Подводя итог, можно сказать, что сам алгоритм состоит из двух ключевых этапов:
- Обучение для получения векторов слов W, весов softmax U, b и векторов абзацев D для уже просмотренных абзацев.
- Этап вывода заключается в получении векторов абзацев D для новых абзацев (ранее не встречавшихся) путем добавления дополнительных столбцов в D и нисходящего градиента на D с удержанием W, U и смешанного.
Мы используем изученные векторы абзацев для прогнозирования некоторых конкретных меток с помощью стандартного классификатора, например, логистической регрессии.
Реализация Doc2Vec на Python
Ниже представлена простая реализация Doc2Vec.
- Сначала мы токенизируем слова в каждом документе и преобразуем их в нижний регистр.
- Затем мы создаем объекты TaggedDocument, необходимые для обучения модели Doc2Vec. Каждый документ связан с уникальным тегом (идентификатором документа). Это вектор абзаца.
- Параметры (vector_size, window, min_count, workers, epochs) управляют размерами модели, размером контекстного окна, минимальным количеством слов, распараллеливанием и эпохами обучения.
- Затем мы выводим векторное представление для нового документа, который не был частью обучающих данных.
- Затем мы вычисляем показатель сходства.
from gensim.models.doc2vec import Doc2Vec, TaggedDocument
from nltk.tokenize import word_tokenize
import nltk
nltk.download('punkt')
# Sample data
data = ["The movie is awesome. It was a good thriller",
"We are learning NLP throughg GeeksforGeeks",
"The baby learned to walk in the 5th month itself"]
# Tokenizing the data
tokenized_data = [word_tokenize(document.lower()) for document in data]
# Creating TaggedDocument objects
tagged_data = [TaggedDocument(words=words, tags=[str(idx)])
for idx, words in enumerate(tokenized_data)]
# Training the Doc2Vec model
model = Doc2Vec(vector_size=100, window=2, min_count=1, workers=4, epochs=1000)
model.build_vocab(tagged_data)
model.train(tagged_data, total_examples=model.corpus_count,
epochs=model.epochs)
# Infer vector for a new document
new_document = "The baby was laughing and palying"
print('Original Document:', new_document)
inferred_vector = model.infer_vector(word_tokenize(new_document.lower()))
# Find most similar documents
similar_documents = model.dv.most_similar(
[inferred_vector], topn=len(model.dv))
# Print the most similar documents
for index, score in similar_documents:
print(f"Document {index}: Similarity Score: {score}")
print(f"Document Text: {data[int(index)]}")
print()
Выход:
Оригинальный документ: Ребенок смеялся и играл
Документ 2: Оценка сходства: 0,9838361740112305
Текст документа: Ребенок научился ходить на 5-м месяце
Документ 0: Оценка сходства: 0,9455077648162842
Текст документа: Фильм потрясающий. Это был хороший триллер
Документ 1: Оценка сходства: 0,8828089833259583
Текст документа: Мы изучаем НЛП через GeeksforGeeks
СБЕРТ
SBERT добавляет операцию объединения к выходу BERT для получения вложений предложений фиксированного размера. Предложение преобразуется в вложение слов и передается через сеть BERT для получения вектора контекста. Исследователи экспериментировали с различными вариантами объединения, но обнаружили, что в среднем объединение работает лучше всего. Затем вектор контекста усредняется для получения вложений предложений.
SBERT использует три целевые функции для обновления весов модели BERT. Модель Bert структурирована по-разному в зависимости от типа обучающих данных, которые управляют целевой функцией.
1. Классификационная цель Функция
- Эта архитектура модели использует часть предложения вместе с метками в качестве обучающих данных.
- Здесь модель Bert структурирована как сиамская сеть. Что такое сиамская сеть? Она состоит из двух идентичных подсетей, каждая из которых является моделью BERT. Две модели разделяют/имеют одинаковые параметры/веса. Обновление параметров зеркально отражается в обеих подмоделях. Наверху слоя опроса у нас есть softmax-классификатор с числом узлов, равным числу меток данных обучения.
- Предложения передаются вместе, чтобы получить вложения предложений u и v вместе с поэлементно различными |uv|. Эти три вектора (u, v,|uv|) умножаются на весовой вектор W размером (3n*K), чтобы получить классификацию softmax.
Здесь,- n — размерность вложений предложений
- k количество меток.
- Оптимизация выполняется с использованием потери кросс-энтропии.

2. Целевая функция регрессии
Это также использует пару предложений с метками в качестве обучающих данных. Сеть также структурирована как сиамская сеть. Однако вместо слоя softmax для вычисления косинусного сходства используется выход слоя объединения, а среднеквадратичная ошибка потерь используется в качестве целевой функции для обучения весов модели BERT.

3. Триплетная целевая функция
Здесь модель структурирована как триплетные сети.
- В триплетной сети три подсети обрабатывают якорное предложение, положительное (похожее) предложение и отрицательное (несхожее) предложение. Модель учится минимизировать расстояние между якорным и положительным предложениями, одновременно максимизируя расстояние между якорным и отрицательным предложениями.
- Для обучения модели нам нужен набор данных, который содержит якорный набор данных a, положительное предложение p и отрицательное предложение n. Примером такого набора данных является «Набор данных триплетов раздела Википедии».
Математически мы минимизируем следующую функцию потерь.
- с sa, sp, sn предложение встраивание для a/n/p,
- || · || метрика расстояния и маржа.
- Запас ϵ гарантирует, что sp по крайней мере ближе к sa, чем sn.
Реализация Python
Для реализации нам сначала нужно установить фреймворк Sentence transformer.
!pip install -U предложения-трансформеры- Класс SentenceTransformer используется для загрузки модели SBERT.
- Класс SentenceTransformer используется для загрузки модели SBERT.
- Мы используем косинусное расстояние scipy для вычисления расстояния между двумя векторами. Чтобы получить сходство, мы вычитаем его из 1
#!pip install -U sentence-transformers
from scipy.spatial import distance
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
# Sample sentence
sentences = ["The movie is awesome. It was a good thriller",
"We are learning NLP throughg GeeksforGeeks",
"The baby learned to walk in the 5th month itself"]
test = "I liked the movie."
print('Test sentence:',test)
test_vec = model.encode([test])[0]
for sent in sentences:
similarity_score = 1-distance.cosine(test_vec, model.encode([sent])[0])
print(f'\nFor {sent}\nSimilarity Score = {similarity_score} ')
Выход:
Тестовое предложение: Мне понравился фильм.
Фильм потрясающий. Это был хороший триллер.
Оценка сходства = 0,682051956653595.
Для Мы изучаем НЛП через GeeksforGeeks.
Оценка сходства = 0,0878136083483696.
Для Ребенок научился ходить на 5-м месяце.
Оценка сходства = 0,04816452041268349.
Вывод
Структура состоит из двух компонентов:
Кодировщик предложений
- Первый — это кодировщик предложений, отвечающий за получение векторов слов и преобразование предложений в закодированные векторы.
- InferSent начинает с предварительно обученных вложений слов. Слова встраиваются в непрерывные векторные представления.
- Эти внедрения служат входными данными для двунаправленной LSTM-сети , способной фиксировать последовательную информацию и зависимости в данных.
- После обработки входного предложения через двунаправленную LSTM применяется слой пулинга для получения векторного представления фиксированного размера всего предложения. Распространенные методы пулинга включают максимальный пул, средний пул или конкатенацию конечных скрытых состояний.
Классификатор
- Объединенное представление предложений пропускается через один или несколько полностью связанных слоев.
- Эти слои служат частью классификатора, которая принимает объединенные закодированные векторы в качестве входных данных и генерирует классификацию, определяя, является ли связь между предложениями следствием, противоречием или нейтральной.
- Для извлечения связей между парами предложений u и v применяются 3 метода сопоставления.
- конкатенация двух представлений (u, v)
- поэлементное произведение u * v
- поэлементная разность |u — v |
- Полученный вектор подается в 3-классовый классификатор (вывод, противоречие и нейтральность), состоящий из нескольких полностью связанных слоев, за которыми следует слой SoftMax.

Реализация Python
Реализация выведенной модели — немного длительный процесс, поскольку стандартного API для обнимающего лица не существует. Infersent поставляется в двух версиях. Версия 1 обучается с помощью Glove, а версия 2 — с помощью FastText wordembedding . Мы будем использовать версию 2, поскольку ее загрузка и обработка занимает меньше времени.
Сначала нам нужно построить выводимую модель. Нижеприведенный класс делает это. Он был взят из модели Python Zoo.
#Infersenct model class # copied from infersent github
%load_ext autoreload
%autoreload 2
%matplotlib inline
from random import randint
import numpy as np
import torch
import time
import torch.nn as nn
class InferSent(nn.Module):
def __init__(self, config):
super(InferSent, self).__init__()
self.bsize = config['bsize']
self.word_emb_dim = config['word_emb_dim']
self.enc_lstm_dim = config['enc_lstm_dim']
self.pool_type = config['pool_type']
self.dpout_model = config['dpout_model']
self.version = 1 if 'version' not in config else config['version']
self.enc_lstm = nn.LSTM(self.word_emb_dim, self.enc_lstm_dim, 1,
bidirectional=True, dropout=self.dpout_model)
assert self.version in [1, 2]
if self.version == 1:
self.bos = '<s>'
self.eos = '</s>'
self.max_pad = True
self.moses_tok = False
elif self.version == 2:
self.bos = '<p>'
self.eos = '</p>'
self.max_pad = False
self.moses_tok = True
def is_cuda(self):
# either all weights are on cpu or they are on gpu
return self.enc_lstm.bias_hh_l0.data.is_cuda
def forward(self, sent_tuple):
# sent_len: [max_len, ..., min_len] (bsize)
# sent: (seqlen x bsize x worddim)
sent, sent_len = sent_tuple
# Sort by length (keep idx)
sent_len_sorted, idx_sort = np.sort(sent_len)[::-1], np.argsort(-sent_len)
sent_len_sorted = sent_len_sorted.copy()
idx_unsort = np.argsort(idx_sort)
idx_sort = torch.from_numpy(idx_sort).cuda() if self.is_cuda() \
else torch.from_numpy(idx_sort)
sent = sent.index_select(1, idx_sort)
# Handling padding in Recurrent Networks
sent_packed = nn.utils.rnn.pack_padded_sequence(sent, sent_len_sorted)
sent_output = self.enc_lstm(sent_packed)[0] # seqlen x batch x 2*nhid
sent_output = nn.utils.rnn.pad_packed_sequence(sent_output)[0]
# Un-sort by length
idx_unsort = torch.from_numpy(idx_unsort).cuda() if self.is_cuda() \
else torch.from_numpy(idx_unsort)
sent_output = sent_output.index_select(1, idx_unsort)
# Pooling
if self.pool_type == "mean":
sent_len = torch.FloatTensor(sent_len.copy()).unsqueeze(1).cuda()
emb = torch.sum(sent_output, 0).squeeze(0)
emb = emb / sent_len.expand_as(emb)
elif self.pool_type == "max":
if not self.max_pad:
sent_output[sent_output == 0] = -1e9
emb = torch.max(sent_output, 0)[0]
if emb.ndimension() == 3:
emb = emb.squeeze(0)
assert emb.ndimension() == 2
return emb
def set_w2v_path(self, w2v_path):
self.w2v_path = w2v_path
def get_word_dict(self, sentences, tokenize=True):
# create vocab of words
word_dict = {}
sentences = [s.split() if not tokenize else self.tokenize(s) for s in sentences]
for sent in sentences:
for word in sent:
if word not in word_dict:
word_dict[word] = ''
word_dict[self.bos] = ''
word_dict[self.eos] = ''
return word_dict
def get_w2v(self, word_dict):
assert hasattr(self, 'w2v_path'), 'w2v path not set'
# create word_vec with w2v vectors
word_vec = {}
with open(self.w2v_path) as f:
for line in f:
word, vec = line.split(' ', 1)
if word in word_dict:
word_vec[word] = np.fromstring(vec, sep=' ')
print('Found %s(/%s) words with w2v vectors' % (len(word_vec), len(word_dict)))
return word_vec
def get_w2v_k(self, K):
assert hasattr(self, 'w2v_path'), 'w2v path not set'
# create word_vec with k first w2v vectors
k = 0
word_vec = {}
with open(self.w2v_path) as f:
for line in f:
word, vec = line.split(' ', 1)
if k <= K:
word_vec[word] = np.fromstring(vec, sep=' ')
k += 1
if k > K:
if word in [self.bos, self.eos]:
word_vec[word] = np.fromstring(vec, sep=' ')
if k > K and all([w in word_vec for w in [self.bos, self.eos]]):
break
return word_vec
def build_vocab(self, sentences, tokenize=True):
assert hasattr(self, 'w2v_path'), 'w2v path not set'
word_dict = self.get_word_dict(sentences, tokenize)
self.word_vec = self.get_w2v(word_dict)
print('Vocab size : %s' % (len(self.word_vec)))
# build w2v vocab with k most frequent words
def build_vocab_k_words(self, K):
assert hasattr(self, 'w2v_path'), 'w2v path not set'
self.word_vec = self.get_w2v_k(K)
print('Vocab size : %s' % (K))
def update_vocab(self, sentences, tokenize=True):
assert hasattr(self, 'w2v_path'), 'warning : w2v path not set'
assert hasattr(self, 'word_vec'), 'build_vocab before updating it'
word_dict = self.get_word_dict(sentences, tokenize)
# keep only new words
for word in self.word_vec:
if word in word_dict:
del word_dict[word]
# udpate vocabulary
if word_dict:
new_word_vec = self.get_w2v(word_dict)
self.word_vec.update(new_word_vec)
else:
new_word_vec = []
print('New vocab size : %s (added %s words)'% (len(self.word_vec), len(new_word_vec)))
def get_batch(self, batch):
# sent in batch in decreasing order of lengths
# batch: (bsize, max_len, word_dim)
embed = np.zeros((len(batch[0]), len(batch), self.word_emb_dim))
for i in range(len(batch)):
for j in range(len(batch[i])):
embed[j, i, :] = self.word_vec[batch[i][j]]
return torch.FloatTensor(embed)
def tokenize(self, s):
from nltk.tokenize import word_tokenize
if self.moses_tok:
s = ' '.join(word_tokenize(s))
s = s.replace(" n't ", "n 't ") # HACK to get ~MOSES tokenization
return s.split()
else:
return word_tokenize(s)
def prepare_samples(self, sentences, bsize, tokenize, verbose):
sentences = [[self.bos] + s.split() + [self.eos] if not tokenize else
[self.bos] + self.tokenize(s) + [self.eos] for s in sentences]
n_w = np.sum([len(x) for x in sentences])
# filters words without w2v vectors
for i in range(len(sentences)):
s_f = [word for word in sentences[i] if word in self.word_vec]
if not s_f:
import warnings
warnings.warn('No words in "%s" (idx=%s) have w2v vectors. \
Replacing by "</s>"..' % (sentences[i], i))
s_f = [self.eos]
sentences[i] = s_f
lengths = np.array([len(s) for s in sentences])
n_wk = np.sum(lengths)
if verbose:
print('Nb words kept : %s/%s (%.1f%s)' % (
n_wk, n_w, 100.0 * n_wk / n_w, '%'))
# sort by decreasing length
lengths, idx_sort = np.sort(lengths)[::-1], np.argsort(-lengths)
sentences = np.array(sentences)[idx_sort]
return sentences, lengths, idx_sort
def encode(self, sentences, bsize=64, tokenize=True, verbose=False):
tic = time.time()
sentences, lengths, idx_sort = self.prepare_samples(
sentences, bsize, tokenize, verbose)
embeddings = []
for stidx in range(0, len(sentences), bsize):
batch = self.get_batch(sentences[stidx:stidx + bsize])
if self.is_cuda():
batch = batch.cuda()
with torch.no_grad():
batch = self.forward((batch, lengths[stidx:stidx + bsize])).data.cpu().numpy()
embeddings.append(batch)
embeddings = np.vstack(embeddings)
# unsort
idx_unsort = np.argsort(idx_sort)
embeddings = embeddings[idx_unsort]
if verbose:
print('Speed : %.1f sentences/s (%s mode, bsize=%s)' % (
len(embeddings)/(time.time()-tic),
'gpu' if self.is_cuda() else 'cpu', bsize))
return embeddings
def visualize(self, sent, tokenize=True):
sent = sent.split() if not tokenize else self.tokenize(sent)
sent = [[self.bos] + [word for word in sent if word in self.word_vec] + [self.eos]]
if ' '.join(sent[0]) == '%s %s' % (self.bos, self.eos):
import warnings
warnings.warn('No words in "%s" have w2v vectors. Replacing \
by "%s %s"..' % (sent, self.bos, self.eos))
batch = self.get_batch(sent)
if self.is_cuda():
batch = batch.cuda()
output = self.enc_lstm(batch)[0]
output, idxs = torch.max(output, 0)
# output, idxs = output.squeeze(), idxs.squeeze()
idxs = idxs.data.cpu().numpy()
argmaxs = [np.sum((idxs == k)) for k in range(len(sent[0]))]
# visualize model
import matplotlib.pyplot as plt
plt.figure(figsize=(12,12))
x = range(len(sent[0]))
y = [100.0 * n / np.sum(argmaxs) for n in argmaxs]
plt.xticks(x, sent[0], rotation=45)
plt.bar(x, y)
plt.ylabel('%')
plt.title('Visualisation of words importance')
plt.show()
return output, idxs, argmaxs
#Класс модели Infersenct # скопирован из infersenct github% load_ext автоперезагрузка% автоперезагрузка 2% matplotlib встроенныйиз случайного импорта randintимпортировать numpy как npимпортный факел время импортаимпортировать torch . nn как nnкласс InferSent ( nn . Модуль ): def __init__ ( self , config ): super ( InferSent , self ). __init__ () self.bsize = конфигурация [ ' bsize ' ] сам . word_emb_dim = конфигурация [ 'word_emb_dim' ] self.enc_lstm_dim = конфигурация [ ' enc_lstm_dim ' ] self.pool_type = конфигурация [ ' pool_type ' ] self.dpout_model = конфигурация [ ' dpout_model ' ] self . version = 1 если 'version' отсутствует в конфигурации иначе config [ 'version' ] self.enc_lstm = nn.LSTM ( self.word_emb_dim , self.enc_lstm_dim , 1 , двунаправленный = True , выпадение = self . dpout_model ) утвердить себя . версия в [ 1 , 2 ] если self . версия == 1 : сам . bos = '<s>' я . eos = '</s>' сам . max_pad = True сам . moses_tok = Ложь elif self . версия == 2 : сам . bos = '<p>' я . eos = '</p>' self.max_pad = Ложь сам . moses_tok = Правда определение is_cuda ( self ): # либо все веса на процессоре, либо на видеокарте вернуть себя.enc_lstm.bias_hh_l0.data.is_cuda def forward ( self , sent_tuple ): # sent_len: [макс_лен, ..., мин_лен] (bsize) # отправлено: (seqlen x bsize x worddim) отправлено , send_len = send_tuple # Сортировать по длине (сохранить idx) send_len_sorted , idx_sort = np . sort ( send_len )[:: - 1 ], np . argsort ( - send_len ) send_len_sorted = send_len_sorted . копия () idx_unsort = np.argsort ( idx_sort ) idx_sort = torch.from_numpy ( idx_sort ) .cuda ( ) если сам.is_cuda ( ) \ иначе torch.from_numpy ( idx_sort ) отправлено = отправлено.index_select ( 1 , idx_sort ) # Обработка заполнения в рекуррентных сетях sent_packed = nn.utils.rnn.pack_padded_sequence ( отправлено , send_len_sorted ) sent_output = self.enc_lstm ( sent_packed ) [ 0 ] # seqlen x batch x 2* nhid отправленный_вывод = nn.utils.rnn.pad_packed_sequence ( отправленный_вывод ) [ 0 ] # Отменить сортировку по длине idx_unsort = torch.from_numpy ( idx_unsort ) .cuda ( ) если сам.is_cuda ( ) \ иначе torch.from_numpy ( idx_unsort ) отправленный_вывод = отправленный_вывод.индекс_выбор ( 1 , idx_unsort ) # Объединение если self.pool_type == " mean " : sent_len = torch.FloatTensor ( sent_len.copy ( ) ) . unsqueeze ( 1 ) .cuda ( ) emb = torch.sum ( sent_output , 0 ) .squeeze ( 0 ) emb = emb / sent_len . expand_as ( emb ) elif self.pool_type == " max " : если не self . max_pad : отправленный_вывод [ отправленный_вывод == 0 ] = - 1e9 emb = torch.max ( sent_output , 0 ) [ 0 ] если emb.ndimension ( ) == 3 : emb = emb . squeeze ( 0 ) утвердить emb.ndimension ( ) == 2 вернуть наб def set_w2v_path ( self , w2v_path ): сам . w2v_path = w2v_path def get_word_dict ( self , sentences , tokenize = True ): # создать словарь слов word_dict = {} предложения = [ s . split () если не токенизировать иначе self . tokenize ( s ) для s в предложениях ] для отправленных предложений : для слова в отправлено : если слово отсутствует в word_dict : word_dict [ слово ] = '' word_dict [ self.bos ] = ' ' word_dict [ self.eos ] = ' ' вернуть word_dict def get_w2v ( self , word_dict ): assert hasattr ( self , 'w2v_path' ), 'w2v path не установлен' # создаем word_vec с векторами w2v word_vec = {} с открытым ( self . w2v_path ) как f : для строки в f : слово , век = строка . разделить ( '' , 1 ) если слово в word_dict : word_vec [ word ] = np.fromstring ( vec , sep = ' ' ) print ( 'Найдено %s(/%s) слов с векторами w2v' % ( len ( word_vec ), len ( word_dict ))) вернуть word_vec def get_w2v_k ( self , K ): assert hasattr ( self , 'w2v_path' ), 'w2v path не установлен' # создаем word_vec с k первыми векторами w2v к = 0 word_vec = {} с открытым ( self . w2v_path ) как f : для строки в f : слово , век = строка . разделить ( '' , 1 ) если к <= К : word_vec [ word ] = np.fromstring ( vec , sep = ' ' ) к += 1 если к > К : если слово в [ self.bos , self.eos ] : word_vec [ word ] = np.fromstring ( vec , sep = ' ' ) если k > K и все ( [ w in word_vec for w in [ self.bos , self.eos ] ] ) : перерыв вернуть word_vec def build_vocab ( self , sentences , tokenize = True ): assert hasattr ( self , 'w2v_path' ), 'w2v path не установлен' word_dict = self.get_word_dict ( предложения , токенизация ) self.word_vec = self.get_w2v ( word_dict ) print ( 'Размер словаря : %s' % ( len ( self . word_vec ))) # построить w2v vocab с k наиболее часто встречающимися словами def build_vocab_k_words ( self , K ): assert hasattr ( self , 'w2v_path' ), 'w2v path не установлен' self.word_vec = self.get_w2v_k ( K ) print ( 'Размер словаря : %s' % ( K )) def update_vocab ( self , sentences , tokenize = True ): assert hasattr ( self , 'w2v_path' ), 'предупреждение: путь w2v не установлен' assert hasattr ( self , 'word_vec' ), 'build_vocab перед его обновлением' word_dict = self.get_word_dict ( предложения , токенизация ) # оставить только новые слова для слова в self . word_vec : если слово в word_dict : del word_dict [ слово ] # обновить словарь если word_dict : new_word_vec = self.get_w2v ( word_dict ) self.word_vec.обновить ( new_word_vec ) еще : new_word_vec = [] print ( 'Новый размер словаря : %s (добавлено %s слов)' % ( len ( self . word_vec ), len ( new_word_vec ))) def get_batch ( self , batch ): # отправлено в пакетном порядке в порядке убывания длины # пакет: (bsize, max_len, word_dim) embed = np . zeros (( len ( batch [ 0 ]), len ( batch ), self . word_emb_dim )) для i в диапазоне ( len ( batch )): для j в диапазоне ( len ( batch [ i ])): встроить [ j , i , :] = self.word_vec [ batch [ i ] [ j ]] вернуть факел . FloatTensor ( встроить ) def tokenize ( self , s ): из nltk . tokenize импорт word_tokenize если сам . moses_tok : s = ' ' . присоединиться ( word_tokenize ( s )) s = s . replace ( " n't " , " n 't " ) # Взлом для получения токенизации ~MOSES возврат s . split () еще : вернуть word_tokenize ( s ) def prepare_samples ( self , sentences , bsize , tokenize , verbose ): предложения = [[ self . bos ] + s . split () + [ self . eos ] если не токенизировать иначе [ self.bos ] + self.tokenize ( s ) + [ self.eos ] для s в предложениях ] n_w = np . sum ([ len ( x ) для x в предложениях ]) # фильтрует слова без векторов w2v для i в диапазоне ( len ( предложения )): s_f = [ слово в слово в предложениях [ i ] если слово в self . word_vec ] если не s_f : импорт предупреждений предупреждения . warn ( 'Ни одно слово в "%s" (idx=%s) не имеет векторов w2v. \ Замена на "</s>"..' % ( предложения [ i ], i )) s_f = [ self.eos ] предложения [ i ] = s_f длины = np . массив ([ len ( s ) для s в предложениях ]) n_wk = np.sum ( длины ) если многословно : print ( 'Количество сохраненных слов: %s/%s (%.1f%s)' % ( n_wk , n_w , 100,0 * n_wk / n_w , '%' )) # сортировка по убыванию длины длины , idx_sort = np.sort ( длины ) [ :: - 1 ] , np.argsort ( -длины ) предложения = np.массив ( предложения ) [ idx_sort ] возвращаемые предложения , длины , idx_sort def encode ( self , sentences , bsize = 64 , tokenize = True , verbose = False ): тик = время . время () предложения , длины , idx_sort = self . prepare_samples ( предложения , bsize , токенизация , многословный ) вложения = [] для stidx в диапазоне ( 0 , len ( предложения ), bsize ): batch = self.get_batch ( предложения [ stidx : stidx + bsize ] ) если сам . is_cuda (): партия = партия . cuda () с факелом . no_grad (): batch = self.forward (( batch , lengths [ stidx : stidx + bsize ])) . data.cpu ( ) . numpy ( ) вложения . добавление ( пакет ) вложения = np . vstack ( вложения ) # отменить сортировку idx_unsort = np.argsort ( idx_sort ) вложения = вложения [ idx_unsort ] если многословно : print ( 'Скорость: %.1f предложений/с (режим %s, bsize=%s)' % ( len ( вложения ) / ( время.время ( ) - тик ) , 'gpu' если сам . is_cuda () иначе 'cpu' , bsize )) возврат вложений def visualize ( self , sent , tokenize = True ): отправлено = отправлено . split () если не токенизировать иначе сам . токенизировать ( отправлено ) отправлено = [[ self . bos ] + [ слово в слово в отправлено если слово в self . word_vec ] + [ self . eos ]] если ' ' . join ( отправлено [ 0 ]) == '%s %s' % ( self . bos , self . eos ): импорт предупреждений предупреждения . предупреждение ( 'Ни одно слово в "%s" не имеет векторов w2v. Замена \ от "%s %s"..' % ( отправлено , сам . bos , сам . eos )) партия = сам . получить_партию ( отправлено ) если сам . is_cuda (): партия = партия . cuda () выход = self.enc_lstm ( пакет ) [ 0 ] выход , idxs = torch.max ( выход , 0 ) # вывод, idxs = вывод.squeeze(), idxs.squeeze() idxs = idxs.data.cpu ( ) . numpy ( ) argmaxs = [ np . sum (( idxs == k )) для k в диапазоне ( len ( sent [ 0 ]))] # визуализировать модель импортировать matplotlib . pyplot как plt plt . рисунок ( figsize = ( 12 , 12 )) x = диапазон ( len ( отправлено [ 0 ])) y = [ 100,0 * n / np . сумма ( argmaxs ) для n в argmaxs ] plt.xticks ( x , отправлено [ 0 ] , вращение = 45 ) пл . бар ( x , y ) plt.ylabel ( ' % ' ) plt . title ( 'Визуализация важности слов' ) plt . показать () возврат вывода , idxs , argmaxsДалее мы загружаем самые современные вложения fastText и загружаем предварительно обученные модели.
!mkdir fastText
!curl -Lo fastText/crawl-300d-2M.vec.zip https://dl.fbaipublicfiles.com/fasttext/vectors-english/crawl-300d-2M.vec.zip
!unzip fastText/crawl-300d-2M.vec.zip -d fastText/
!mkdir encoder
!curl -Lo encoder/infersent2.pkl https://dl.fbaipublicfiles.com/infersent/infersent2.pkl
! mkdir fastText! curl - Lo fastText / crawl - 300 d - 2 M . vec . zip https : // dl . fbaipublicfiles . com / fasttext / vectors - english / crawl - 300 d - 2 M . vec . zip! unzip fastText / crawl - 300 d - 2 M . vec . zip - d fastText /! mkdir кодировщик! curl - Lo encoder / infersent2 . pkl https : // dl . fbaipublicfiles . com / infersent / infersent2 . pkl- Код загружает модель токенизатора Punkt из NLTK , которая используется для токенизации .
- Затем мы задаем путь (MODEL_PATH) к файлу предварительно обученной модели InferSent и загружаем ее веса в экземпляр класса InferSent.
- Указываем путь к нашим вложениям слов (W2V_PATH)
- Метод build_vocab_k_words вызывается для загрузки вложений для K наиболее часто встречающихся слов (в данном случае — 100 000 самых популярных слов).
import nltk
nltk.download('punkt')
MODEL_PATH = 'encoder/infersent2.pkl'
params_model = {'bsize': 64, 'word_emb_dim': 300, 'enc_lstm_dim': 2048,
'pool_type': 'max', 'dpout_model': 0.0}
model = InferSent(params_model)
model.load_state_dict(torch.load(MODEL_PATH))
W2V_PATH = 'fastText/crawl-300d-2M.vec'
model.set_w2v_path(W2V_PATH)
# Load embeddings of K most frequent words
model.build_vocab_k_words(K=100000)
импорт нлткnltk . скачать ( 'punkt' )MODEL_PATH = 'encoder/infersent2.pkl' params_model = { 'bsize' : 64 , 'word_emb_dim' : 300 , 'enc_lstm_dim' : 2048 , 'pool_type' : 'max' , 'dpout_model' : 0.0 }модель = InferSent ( params_model )модель.load_state_dict ( факел.load ( MODEL_PATH ) )W2V_PATH = 'fastText/crawl-300d-2M.vec'модель . set_w2v_path ( W2V_PATH )# Загрузить вложения K наиболее частых словмодель . build_vocab_k_words ( K = 100000 )
Затем мы используем модель для вывода
# Sample sentence
from scipy.spatial import distance
sentences = ["The movie is awesome. It was a good thriller",
"We are learning NLP throughg GeeksforGeeks",
"The baby learned to walk in the 5th month itself"]
test = "I liked the movie"
print('Test Sentence:', test)
test_vec = model.encode([test])[0]
for sent in sentences:
similarity_score = 1-distance.cosine(test_vec, model.encode([sent])[0])
print(f'\nFor {sent}\nSimilarity Score = {similarity_score} ')
Выход:
Тестовое предложение: Мне понравился фильм
Фильм потрясающий. Это был хороший триллер
Оценка сходства = 0,5299297571182251
Мы изучаем НЛП через GeeksforGeeks
Оценка сходства = 0,33156681060791016
Ребенок научился ходить на 5-м месяце жизни
Оценка сходства = 0,20128820836544037
USE — универсальный кодировщик предложений
На высоком уровне он состоит из кодировщика, который обобщает любое предложение, чтобы получить вложение предложения, которое можно использовать для любой задачи обработки естественного языка.
Часть кодировщика поставляется в двух вариантах, и любой из них может быть использован
- Transformer - Здесь используется часть кодировщика оригинальной архитектуры transformer . Архитектура состоит из 6 сложенных слоев transformer. Каждый слой имеет модуль внутреннего внимания, за которым следует сеть прямой связи . Выходные контекстно-зависимые вложения слов добавляются поэлементно и делятся на квадратный корень длины предложения для учета разницы в длине предложения. Мы получаем 512-мерный вектор в качестве выходного вложения предложения.
- Глубокая усредняющая сеть — вложения для слов и биграмм, присутствующих в предложении, усредняются вместе. Затем они пропускаются через 4-слойную глубокую DNN с прямой связью, чтобы получить 512-мерное вложение предложения в качестве выходных данных. Вложения для слов и биграмм изучаются во время обучения.
Подготовка к ЕГЭ
USE обучается выполнению различных контролируемых и неконтролируемых заданий, таких как Skipthoughts, NLI и других, с использованием следующих принципов.
- Токенизировать предложения после преобразования их в строчные буквы
- В зависимости от типа кодировщика предложение преобразуется в 512-мерный вектор.
- Полученные вложения предложений впоследствии используются для обновления параметров модели.
- Затем обученная модель снова применяется для создания нового 512-мерного вложения предложения.

Реализация Python
Мы загружаем модуль TF Hub универсального кодировщика предложений.
- module_url содержит URL-адрес для загрузки универсального кодировщика предложений (версии 4) из TensorFlow Hub.
- Функция hub.load используется для загрузки модели Universal Sentence Encoder с указанного URL (module_url).
- Мы определяем функцию с именем embed, которая принимает входной текст и возвращает встраивания, используя загруженную модель универсального кодировщика предложений.
import tensorflow as tf
import tensorflow_hub as hub
import matplotlib.pyplot as plt
import numpy as np
import os
import pandas as pd
import re
import seaborn as sns
module_url = "https://tfhub.dev/google/universal-sentence-encoder/4"
model = hub.load(module_url)
print("module %s loaded" % module_url)
def embed(input):
return model(input)
Ниже мы рассчитаем степень сходства на основе наших выборочных данных.
from scipy.spatial import distance
test = ["I liked the movie very much"]
print('Test Sentence:',test)
test_vec = embed(test)
# Sample sentence
sentences = [["The movie is awesome and It was a good thriller"],
["We are learning NLP throughg GeeksforGeeks"],
["The baby learned to walk in the 5th month itself"]]
for sent in sentences:
similarity_score = 1-distance.cosine(test_vec[0,:],embed(sent)[0,:])
print(f'\nFor {sent}\nSimilarity Score = {similarity_score} ')
Выход
Тестовое предложение: ['Мне очень понравился фильм']
Для ['Фильм потрясающий, и это был хороший триллер']
Оценка сходства = 0,6519516706466675
Для ['Мы изучаем НЛП с помощью GeeksforGeeks']
Оценка сходства = 0,06988027691841125
Для ['Ребенок научился ходить на 5-м месяце']
Оценка сходства = -0,01121298223733902
Заключение
В этой статье мы разобрались с семантическим сходством и его применением. Мы увидели архитектуру 4 лучших моделей встраивания предложений, используемых для расчета семантического сходства предложений, и их реализацию на Python.